"""
Objects representing PDF path (stroke and filling) extracted from pdf drawings and annotations.
表示从PDF绘图和注释中提取的PDF路径(描边和填充)的对象。

Data structure based on results of ``page.get_drawings()``::
基于``page.get_drawings()``结果的数据结构::

    {
        'color': (x,x,x) or None,  # stroke color
        'fill' : (x,x,x) or None,  # fill color
        'width': float,            # line width
        'closePath': bool,         # whether to connect last and first point
        'rect' : rect,             # page area covered by this path
        'items': [                 # list of draw commands: lines, rectangle or curves.
            ("l", p1, p2),         # a line from p1 to p2
            ("c", p1, p2, p3, p4), # cubic Bézier curve from p1 to p4, p2 and p3
                                   # are the control points
            ("re", rect),          # a rect represented with two diagonal points
            ("qu", quad)           # a quad represented with four corner points
        ],
        ...
    }

References:
参考:
    - https://pymupdf.readthedocs.io/en/latest/page.html#Page.get_drawings
    - https://pymupdf.readthedocs.io/en/latest/faq.html#extracting-drawings

.. note::
    The coordinates extracted by ``page.get_drawings()`` is based on **real** page CS,
    i.e. with rotation considered. This is different from ``page.get_text('rawdict')``.
    由``page.get_drawings()``提取的坐标基于**真实**页面坐标系，
    即考虑了旋转。这与``page.get_text('rawdict')``不同。
"""

import fitz
from ..common.share import rgb_value
from ..common import constants


class Segment:
    """A segment of path, e.g. a line or a rectangle or a curve."""

    """路径的一个片段，例如线条、矩形或曲线。"""

    def __init__(self, item):
        self.points = item[1:]

    def to_strokes(self, width: float, color: list):
        return []


class L(Segment):
    """Line path with source ``("l", p1, p2)``."""

    """源为``("l", p1, p2)``的线段路径。"""

    @property
    def length(self):
        """Length of line."""
        """线段的长度。"""
        x0, y0 = self.points[0]
        x1, y1 = self.points[1]
        return ((x1 - x0) ** 2 + (y1 - y0) ** 2) ** 0.5

    def to_strokes(self, width: float, color: list):
        """Convert to stroke dict.
        转换为描边字典。

        Args:
            width (float): Specify width for the stroke.
            width (float): 指定描边的宽度。
            color (list): Specify color for the stroke.
            color (list): 指定描边的颜色。

        Returns:
            list: A list of ``Stroke`` dicts.
            list: 包含``Stroke``字典的列表。

        .. note::
            A line corresponds to one stroke, but considering the consistence,
            the return stroke dict is append to a list. So, the length of list
            is always 1.
            一条线对应一个描边，但考虑到一致性，返回的描边字典被添加到列表中。
            因此，列表的长度始终为1。
        """
        strokes = []
        strokes.append(
            {
                "start": self.points[0],
                "end": self.points[1],
                "width": width,
                "color": rgb_value(color),
            }
        )
        return strokes


class R(Segment):
    """Rect path with source ``("re", rect)``."""

    """源为``("re", rect)``的矩形路径。"""

    def __init__(self, item):
        # corner points
        # NOTE: center line of path without stroke width considered
        # 角点
        # 注意：路径的中心线，未考虑描边宽度
        x0, y0, x1, y1 = item[1]
        self.points = [
            (x0, y0),
            (x1, y0),
            (x1, y1),
            (x0, y1),
            (x0, y0),  # close path
            # 闭合路径
        ]

    def to_strokes(self, width: float, color: list):
        """Convert each edge to stroke dict.
        将每条边转换为描边字典。

        Args:
            width (float): Specify width for the stroke.
            width (float): 指定描边的宽度。
            color (list): Specify color for the stroke.
            color (list): 指定描边的颜色。

        Returns:
            list: A list of ``Stroke`` dicts.
            list: 包含``Stroke``字典的列表。

        .. note::
            One Rect path is converted to a list of 4 stroke dicts.
            一个矩形路径被转换为4个描边字典的列表。
        """
        strokes = []
        for i in range(len(self.points) - 1):
            strokes.append(
                {
                    "start": self.points[i],
                    "end": self.points[i + 1],
                    "width": width * 2.0,  # seems need adjustment by * 2.0
                    # 似乎需要通过*2.0进行调整
                    "color": rgb_value(color),
                }
            )
        return strokes


class Q(R):
    """Quad path with source ``("qu", quad)``."""

    """源为``("qu", quad)``的四边形路径。"""

    def __init__(self, item):
        # four corner points
        # NOTE: center line of path without stroke width considered
        # 四个角点
        # 注意：路径的中心线，未考虑描边宽度
        ul, ur, ll, lr = item[1]
        self.points = [ul, ur, lr, ll, ul]  # close path
        # 闭合路径


class C(Segment):
    """Bezier curve path with source ``("c", p1, p2, p3, p4)``."""

    """源为``("c", p1, p2, p3, p4)``的贝塞尔曲线路径。"""


class Segments:
    """A sub-path composed of one or more segments."""

    """由一个或多个线段组成的子路径。"""

    def __init__(self, items: list, close_path=False):
        self._instances = []  # type: list[Segment]
        for item in items:
            if item[0] == "l":
                self._instances.append(L(item))
            elif item[0] == "c":
                self._instances.append(C(item))
            elif item[0] == "re":
                self._instances.append(R(item))
            elif item[0] == "qu":
                self._instances.append(Q(item))

        # close path
        # 闭合路径
        if close_path:
            item = ("l", self._instances[-1].points[-1], self._instances[0].points[0])
            line = L(item)
            if line.length > 1e-3:
                self._instances.append(line)

    def __iter__(self):
        return (instance for instance in self._instances)

    @property
    def points(self):
        """Connected points of segments."""
        """线段的连接点。"""
        points = []
        for segment in self._instances:
            points.extend(segment.points)
        return points

    @property
    def is_iso_oriented(self):
        """ISO-oriented criterion: the ratio of real area to bbox exceeds 0.9."""
        """等方向标准：实际面积与边界框面积的比率超过0.9。"""
        bbox_area = self.bbox.get_area()
        return bbox_area == 0 or self.area / bbox_area >= constants.FACTOR_MOST

    @property
    def area(self):
        """Calculate segments area with Green formulas. Note the boundary of Bezier curve
        is simplified with its control points.
        用格林公式计算线段面积。注意贝塞尔曲线的边界用其控制点简化。

        * https://en.wikipedia.org/wiki/Shoelace_formula
        """
        points = self.points
        start, end = points[0], points[-1]
        if abs(start[0] - end[0]) + abs(start[1] - end[1]) > 1e-3:
            return 0.0  # open curve
            # 开放曲线

        # closed curve
        # 闭合曲线
        area = 0.0
        for i in range(len(points) - 1):
            x0, y0 = points[i]
            x1, y1 = points[i + 1]
            area += x0 * y1 - x1 * y0

        return abs(area / 2.0)

    @property
    def bbox(self):
        """Calculate segments bbox."""
        """计算线段的边界框。"""
        points = self.points
        x0 = min(points, key=lambda point: point[0])[0]
        y0 = min(points, key=lambda point: point[1])[1]
        x1 = max(points, key=lambda point: point[0])[0]
        y1 = max(points, key=lambda point: point[1])[1]

        # bbox: `round()` is required to avoid float error
        # bbox: 需要`round()`以避免浮点误差
        return fitz.Rect(round(x0, 2), round(y0, 2), round(x1, 2), round(y1, 2))

    def to_strokes(self, width: float, color: list):
        """Convert each segment to a ``Stroke`` dict.
        将每个线段转换为``Stroke``字典。

        Args:
            width (float): Specify stroke width.
            width (float): 指定描边宽度。
            color (list): Specify stroke color.
            color (list): 指定描边颜色。

        Returns:
            list: A list of ``Stroke`` dicts.
            list: 包含``Stroke``字典的列表。
        """
        strokes = []
        for segment in self._instances:
            strokes.extend(segment.to_strokes(width, color))
        return strokes

    def to_fill(self, color: list):
        """Convert segment closed area to a ``Fill`` dict.
        将线段闭合区域转换为``Fill``字典。

        Args:
            color (list): Specify fill color.
            color (list): 指定填充颜色。

        Returns:
            dict: ``Fill`` dict.
            dict: ``Fill``字典。
        """
        return {"bbox": list(self.bbox), "color": rgb_value(color)}


class Path:
    """Path extracted from PDF, consist of one or more ``Segments``."""

    """从PDF提取的路径，由一个或多个``Segments``组成。"""

    def __init__(self, raw: dict):
        """Init path in real page CS.
        在真实页面坐标系中初始化路径。

        Args:
            raw (dict): Raw dict extracted with `PyMuPDF`, see link
            https://pymupdf.readthedocs.io/en/latest/page.html#Page.get_drawings
            raw (dict): 用`PyMuPDF`提取的原始字典，参见链接
            https://pymupdf.readthedocs.io/en/latest/page.html#Page.get_drawings
        """
        # all path properties
        # 所有路径属性
        self.raw = raw
        self.path_type = raw["type"]  # s, f, or fs

        # always close path if fill, otherwise, depends on property 'closePath'
        # 如果是填充，总是闭合路径，否则，取决于属性'closePath'
        close_path = True if self.is_fill else raw.get("closePath", False)

        # path segments
        # 路径段
        self.items = []  # type: list[Segments]
        self.bbox = fitz.Rect()
        w = raw.get("width", 0.0)
        for segments in self._group_segments(raw["items"]):
            S = Segments(segments, close_path)
            self.items.append(S)

            # update bbox: note iso-oriented line segments -> S.bbox.get_area()==0
            # 更新边界框：注意等方向线段 -> S.bbox.get_area()==0
            rect = S.bbox
            if rect.get_area() == 0:
                rect += (-w, -w, w, w)
            self.bbox |= rect

    @staticmethod
    def _group_segments(items):
        """Group connected segments.
        分组连接的线段。

        Args:
            items (dict): Raw dict extracted from ``page.get_drawings()``.
            items (dict): 从``page.get_drawings()``提取的原始字典。

        Returns:
            list: A list of segments list.
            list: 线段列表的列表。
        """
        segments, segments_list = [], []
        cursor = None
        for item in items:
            # line or curve segment
            # 线或曲线段
            if item[0] in ("l", "c"):
                start, end = item[1], item[-1]
                # add to segments if:
                # - first point of segments, or
                # - connected to previous segment
                # 添加到线段，如果：
                # - 线段的第一个点，或
                # - 连接到前一个线段
                if not segments or start == cursor:
                    segments.append(item)

                # otherwise, close current segments and start a new one
                # 否则，关闭当前线段并开始一个新的
                else:
                    segments_list.append(segments)
                    segments = [item]

                # update current point
                # 更新当前点
                cursor = end

            # rectangle / quad as a separate segments group
            # 矩形/四边形作为单独的线段组
            elif item[0] in ("re", "qu"):
                # close current segments
                # 关闭当前线段
                if segments:
                    segments_list.append(segments)
                    segments = []
                # add this segment
                # 添加此线段
                segments_list.append([item])

        # add last segments if exists
        # 如果存在，添加最后的线段
        if segments:
            segments_list.append(segments)

        return segments_list

    @property
    def is_stroke(self):
        return "s" in self.path_type

    @property
    def is_fill(self):
        return "f" in self.path_type

    @property
    def is_iso_oriented(self):
        """It is iso-oriented when all contained segments are iso-oriented."""
        """当所有包含的线段都是等方向的时，它是等方向的。"""
        for segments in self.items:
            if not segments.is_iso_oriented:
                return False
        return True

    def to_shapes(self):
        """Convert path to ``Shape`` raw dicts.
        将路径转换为``Shape``原始字典。

        Returns:
            list: A list of ``Shape`` dict.
            list: 包含``Shape``字典的列表。
        """
        iso_shapes = []

        # convert to strokes
        # 转换为描边
        white = (1, 1, 1)
        if self.is_stroke:
            stroke_color = self.raw.get("color", white)  # white stroke by default
            # 默认为白色描边
            width = self.raw.get("width", 0.0)
            iso_shapes.extend(self._to_strokes(width, stroke_color))

        # convert to rectangular fill
        # 转换为矩形填充
        if self.is_fill:
            fill_color = self.raw.get("fill", white)  # white filling by default
            # 默认为白色填充
            iso_shapes.extend(self._to_fills(fill_color))

        return iso_shapes

    def _to_strokes(self, width: float, color: list):
        """Convert path to ``Stroke`` raw dicts.
        将路径转换为``Stroke``原始字典。

        Returns:
            list: A list of ``Stroke`` dict.
            list: 包含``Stroke``字典的列表。
        """
        strokes = []
        for segments in self.items:
            strokes.extend(segments.to_strokes(width, color))
        return strokes

    def _to_fills(self, color: list):
        """Convert path to ``Fill`` raw dicts.
        将路径转换为``Fill``原始字典。

        Returns:
            list: A list of ``Fill`` dict.
            list: 包含``Fill``字典的列表。

        .. note::
            The real filling area of this path may be not a rectangle.
            此路径的实际填充区域可能不是矩形。
        """
        fills = []
        for segments in self.items:
            fills.append(segments.to_fill(color))
        return fills

    def plot(self, canvas):
        """Plot path for debug purpose.
        为调试目的绘制路径。

        Args:
            canvas: ``PyMuPDF`` drawing canvas by ``page.new_shape()``.
            canvas: 通过``page.new_shape()``创建的``PyMuPDF``绘图画布。

        Reference:
        参考：

            https://pymupdf.readthedocs.io/en/latest/faq.html#extracting-drawings
        """
        # draw each entry of the 'items' list
        # 绘制'items'列表中的每个条目
        for item in self.raw.get("items", []):
            if item[0] == "l":  # line
                # 线
                canvas.draw_line(item[1], item[2])
            elif item[0] == "re":  # rectangle
                # 矩形
                canvas.draw_rect(item[1])
            elif item[0] == "qu":  # quad
                # 四边形
                canvas.draw_quad(item[1])
            elif item[0] == "c":  # curve
                # 曲线
                canvas.draw_bezier(item[1], item[2], item[3], item[4])
            else:
                raise ValueError("unhandled drawing", item)

        # now apply the common properties to finish the path
        # 现在应用通用属性来完成路径
        canvas.finish(
            fill=self.raw.get("fill", None),  # fill color
            # 填充颜色
            color=self.raw.get("color", None),  # line color
            # 线条颜色
            dashes=self.raw.get("dashes", None),  # line dashing
            # 线条虚线
            even_odd=self.raw.get("even_odd", False),  # control color of overlaps
            # 控制重叠区域的颜色
            closePath=self.raw.get(
                "closePath", False
            ),  # whether to connect last and first point
            # 是否连接最后和第一个点
            lineJoin=self.raw.get("lineJoin", 0),  # how line joins should look like
            # 线条连接的外观
            lineCap=(
                max(self.raw["lineCap"]) if "lineCap" in self.raw else 0
            ),  # how line ends should look like
            # 线条末端的外观
            width=self.raw.get("width", 1),  # line width
            # 线条宽度
            stroke_opacity=self.raw.get("stroke_opacity", 1),  # same value for both
            # 两者使用相同的值
            fill_opacity=self.raw.get("fill_opacity", 1),  # opacity parameters
            # 不透明度参数
        )
